﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Diagnostics;


namespace FileTreeSpace
{
    public class FileHash : object
    {
        private byte[] md5hash;
        private long size;

        public FileHash () { md5hash = null; size = 0; }
        public FileHash (byte[] newHash) { md5hash = newHash; size = -1; }
        public FileHash (byte[] newHash, int newSize) { md5hash = newHash; size = newSize; }

        public int Length { get { return md5hash.Length; } }
        public long Size { get { return size; } }

        public override string ToString ()
        {
            StringBuilder result = new StringBuilder ("");
            for (int i = 0; i < Length; ++i)
                result.Append (md5hash[i].ToString ("X2"));
            return result.ToString ();
        }

        public override Boolean Equals (System.Object obj)
        {
            if (obj == null)
                return false;

            FileHash x = obj as FileHash;

            if (x == null)
                return false;

            for (int i = 0; i < Length; ++i)
            {
                if (md5hash[i] != x.md5hash[i])
                    return false;
            }
            return true;
        }

        public override int GetHashCode ()
        {
            return base.GetHashCode ();
        }

        public static Boolean operator == (FileHash lhs, FileHash rhs)
        {
            if (System.Object.ReferenceEquals (lhs, rhs))
                return true;

            if ((object) lhs == null || (object) rhs == null)
                return false;

            if ((object) rhs.md5hash == null)
            {
                return (object) lhs.md5hash == null;
            }
            else if ((object) lhs.md5hash == null)
                return false;

            for (int i = 0; i < lhs.Length; ++i)
            {
                if (lhs.md5hash[i] != rhs.md5hash[i])
                    return false;
            }
            return true;
        }

        public static Boolean operator != (FileHash lhs, FileHash rhs)
        {
            return !(lhs == rhs);
        }
    }


    // DirPath element.
    public class DirIndex
    {
        internal DirItem dir;
        internal int index;   // Indexes of next dir (avoids relookups).  If -1 then terminal.

        public DirIndex (DirItem newItem) { dir = newItem; index = -1; }
        public DirIndex (DirItem newItem, int newIndex) { dir = newItem; index = newIndex; }

        public int Index { get { return index; } }
        public DirItem Dir { get { return dir; } }
        public string DirName { get { return dir.Name; } }
    }


    // Vector of name, index pairs.  This is a tree path to a DirItem object.
    public class DirPath
    {
        private int rootIndex = 0;  // not well named - baseIndex
        private int fileIndex = -1;  // If >=0, indicates file, otherwise it's a directory.
        private List<DirIndex> list;
        static int Invalid = -2;

        internal int FileIndex { get { return fileIndex; } }

        public DirPath (DirPath startPath)
        {
            list = new List<DirIndex> (startPath.Count);

            for (int i1 = 0; i1 < startPath.Count; ++i1)
                list.Add (new DirIndex (startPath[i1].dir, startPath[i1].index));

            rootIndex = Count - 1;
            this[rootIndex].index = -2;
        }


        public DirPath (DirRoot targetDir)
        {
            list = new List<DirIndex> ();
            DirIndex di = new DirIndex (targetDir, Invalid);
            list.Add (di);
        }

        // Lookup fully-qualified file name.
        public DirPath (DirRoot root, string findName)
        {
            list = new List<DirIndex> ();
            DirItem dir = root;

            int findLen = findName.Length;
            while (findLen > 0 && findName[findLen - 1] == ' ')
                --findLen;

            if (findLen > 0 && findName[findLen - 1] == '\\')
                --findLen;

            if (findLen == 0)
                goto END;

            int k = 0,
                p1 = 0,
                p2 = dir.Name.Length;

            if (findLen >= p2)
            {
                if (findLen > p2)
                {
                    if (findName[p2] != '\\')
                    {
                        // This is the starting/ending depth for potential iterators.
                        // Not even the root of this tree matches the lookup string.
                        goto END;
                    }
                    ++p2;
                }
                string s1 = findName.Substring (p1, dir.Name.Length).ToLower ();
                if (dir.Name.ToLower () == s1)
                {
                    for (; ; )
                    {
                    TOP:
                        DirIndex di = new DirIndex (dir, -1);
                        list.Add (di);
                        p1 = p2;
                        if (p1 >= findLen)
                            // Complete success.
                            goto END;

                        for (k = 0; k < dir.Dirs.Count; ++k)
                        {
                            Int32 x = dir.Dirs[k].Name.Length;
                            p2 = p1 + x;

                            if (findLen < p2)
                                continue;

                            if (findLen > p2)
                            {
                                if (findName[p2] != '\\')
                                    continue;
                                ++p2;
                            }

                            string s2 = findName.Substring (p1, x).ToLower ();
                            if (dir.Dirs[k].Name.ToLower () == s2)
                            {
                                di.index = k;
                                dir = dir.Dirs[k];
                                goto TOP;
                            }
                        }
                        break;
                    }

                    // Can't find matching directory so attempt file lookup.
                    for (k = 0; k < dir.Files.Count; ++k)
                    {
                        int x = dir.Files[k].Name.Length;

                        if (findLen != p1 + x)
                            continue;

                        string compStr = findName.Substring (p1, x).ToLower ();
                        if (dir.Files[k].Name.ToLower () == compStr)
                        {
                            fileIndex = k;
                            break;
                        }
                    }
                }
            }

        END:
            rootIndex = list.Count - 1;
            return;
        }

        public Boolean Advance ()
        {
            int lvl = Count - 1;

            int i1 = this[lvl].Index;

            if (i1 < 0)
            {
                if (i1 == -2)
                {
                    this[lvl].index = -1;
                    return true;
                }

                if (this[lvl].Dir.Dirs.Count > 0)
                {
                    // Traverse to first child (depth-first).
                    this[lvl].index = 0;
                    Add (this[lvl].Dir.Dirs[0], -1);
                    return true;
                }
            }

            // Traverse to sibling, or ancestor sibling.
            for (; ; )
            {
                if (lvl <= rootIndex)
                    return false;

                int i = this[lvl - 1].Index + 1;
                int k = this[lvl - 1].Dir.Dirs.Count;

                if (i < k)
                {
                    this[lvl - 1].index = i;
                    this[lvl].dir = this[lvl - 1].Dir.Dirs[i];
                    break;
                }

                Trim ();
                --lvl;
                this[lvl].index = -1;
            }

            return true;
        }

        /////

        public DirIndex this[int index] { get { return (DirIndex) list[index]; } }
        public int Count { get { return list.Count; } }
        public String NodeName { get { return this[list.Count - 1].Dir.Name; } }
        public DirItem Leaf { get { return this[list.Count - 1].Dir; } }

        public Boolean IsFile { get { return fileIndex >= 0; } }
        public FileItem File { get { return this[list.Count - 1].Dir.Files[fileIndex]; } }

        public DirItem NodeRef (string newLeafName)
        {
            DirItem newNode = new DirItem (newLeafName);
            DirItem node = this[Count - 1].dir;
            node.Dirs.Add (newNode);
            return newNode;
        }

        public void NodeUnreference ()
        {
            Debug.Assert (list.Count >= 2);
            Debug.WriteLine (@"[NodeUnref]Name=" + this[list.Count - 1].Dir.Name);
            this[list.Count - 2].Dir.Dirs.RemoveAt (list[list.Count - 2].Index);
        }

        public void FileRef (string newFileName, long newSize, DateTime dateModified, FileAttributes newAttributes)
        {
            Leaf.Files.Add (newFileName, newSize, dateModified, newAttributes);
        }

        public void FileUnreference ()
        {
            Debug.Assert (IsFile);
            if (IsFile)
                Leaf.Files.Delete (FileIndex);
        }


        public Boolean HasMore (int dirIndex)
        {
            int i = this[dirIndex - 1].Index;
            int iMax = this[dirIndex - 1].Dir.Dirs.Count - 1;

            return i < iMax;
        }

        // Access full path and file name.
        public String FullName
        {
            get
            {
                int depth = list.Count;
                String result = PathNamePartial (list.Count);

                if (IsFile)
                {
                    int nameIndex = fileIndex;
                    //if (nameIndex >= 0)
                    {
                        result += @"\" + this[depth - 1].Dir.Files[nameIndex].Name;
                    }
                }

                return result;
            }
        }

        public String PathName
        {
            get
            {
                if (list.Count == 0)
                    return "";
                else
                {
                    String s = this[0].Dir.Name;

                    for (int i = 1; i < list.Count; ++i)
                        s += @"\" + this[i].Dir.Name;

                    return s;
                }
            }
        }

        public String PathNamePartial (int depth)
        {
            if (depth == 0)
                return "";

            String result = this[0].Dir.Name;
            for (int lvl = 1; lvl < depth; ++lvl)
            {
                result += @"\" + this[lvl].Dir.Name;
            }

            return result;
        }

        protected void Init ()
        {
            list.RemoveAt (1);
            this[0].index = -1;
        }

        protected void Add (DirItem dir, int index)
        {
            list.Add (new DirIndex (dir, index));
        }

        protected void Trim () { list.RemoveAt (Count - 1); }


        // OnChange event not fired for CalcHash
        public void CalcHash ()
        {
            String fileName = FullName;
            FileStream fs1 = new FileStream (fileName, FileMode.Open, FileAccess.Read);

            FileItem fi = this[Count - 1].Dir.Files[fileIndex];
            MD5 md5hasher = MD5.Create ();
            fi.Hash = new FileHash (md5hasher.ComputeHash (fs1));
            fs1.Close ();
        }

        public IEnumerator GetEnumerator ()
        {
            return new PathEnumerator (this);
        }

        public class PathEnumerator : DirPath, IEnumerator
        {
            public PathEnumerator (DirPath startPath) : base (startPath) { }

            object IEnumerator.Current { get { return (DirPath) this; } }
            public void Reset () { Init (); }

            public Boolean MoveNext ()
            {
                return Advance ();
            }
        }

    }
}
